/***
 *
 * Copyright (c) 2007 Paul Hammant
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holders nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package cn.org.rapid_framework.generator.util.paranamer;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.AccessibleObject;

/**
 * Default implementation of Paranamer reads from a post-compile added field called '__PARANAMER_DATA'
 *
 * @author Paul Hammant
 * @author Mauro Talevi
 * @author Guilherme Silveira
 */
public class DefaultParanamer implements Paranamer {

    private static final String COMMA = ",";
    private static final String SPACE = " ";

    public static final String __PARANAMER_DATA = "v1.0 \n"
	    +"lookupParameterNames java.lang.AccessibleObject methodOrConstructor \n"
	    +"lookupParameterNames java.lang.AccessibleObject,boolean methodOrCtor,throwExceptionIfMissing \n"
	    +"getParameterTypeName java.lang.Class cls\n";

    public DefaultParanamer() {

    }

    public String[] lookupParameterNames(AccessibleObject methodOrConstructor) {

	return lookupParameterNames(methodOrConstructor, true);
    }

    public String[] lookupParameterNames(AccessibleObject methodOrCtor,
	    boolean throwExceptionIfMissing) {

	// Oh for some commonality between Constructor and Method !!
	Class<?>[] types = null;
	Class<?> declaringClass = null;
	String name = null;
	if (methodOrCtor instanceof Method) {
	    Method method = (Method) methodOrCtor;
	    types = method.getParameterTypes();
	    name = method.getName();
	    declaringClass = method.getDeclaringClass();
	} else {
	    Constructor<?> constructor = (Constructor<?>) methodOrCtor;
	    types = constructor.getParameterTypes();
	    declaringClass = constructor.getDeclaringClass();
	    name = "<init>";
	}

	if (types.length==0) {
	    return EMPTY_NAMES;
	}
	final String parameterTypeNames = getParameterTypeNamesCSV(types);
	final String[] names = getParameterNames(declaringClass, parameterTypeNames, name+SPACE);
	if (names==null) {
	    if (throwExceptionIfMissing) {
		throw new ParameterNamesNotFoundException("No parameter names found for class '"
			+declaringClass+"', methodOrCtor "+name+" and parameter types "
			+parameterTypeNames);
	    } else {
		return Paranamer.EMPTY_NAMES;
	    }
	}
	return names;
    }

    private static String[] getParameterNames(Class<?> declaringClass, String parameterTypes,
	    String prefix) {

	String data = getParameterListResource(declaringClass);
	String line = findFirstMatchingLine(data, prefix+parameterTypes+SPACE);
	String[] parts = line.split(SPACE);
	// assumes line structure: constructorName parameterTypes parameterNames
	if (parts.length==3&&parts[1].equals(parameterTypes)) {
	    String parameterNames = parts[2];
	    return parameterNames.split(COMMA);
	}
	return Paranamer.EMPTY_NAMES;
    }

    static String getParameterTypeNamesCSV(Class<?>[] parameterTypes) {

	StringBuffer sb = new StringBuffer();
	for (int i = 0; i<parameterTypes.length; i++) {
	    sb.append(getParameterTypeName(parameterTypes[i]));
	    if (i<parameterTypes.length-1) {
		sb.append(COMMA);
	    }
	}
	return sb.toString();
    }

    private static String getParameterListResource(Class<?> declaringClass) {

	try {
	    Field field = declaringClass.getDeclaredField("__PARANAMER_DATA");
	    // TODO create acc test which finds field?
	    // TODO create acc test which does not find field?
	    // TODO create acc test what to do with private? access anyway?
	    // TODO create acc test with non static field?
	    // TODO create acc test with another type of field?
	    if (!Modifier.isStatic(field.getModifiers())||!field.getType().equals(String.class)) {
		return null;
	    }
	    return (String) field.get(null);
	} catch (NoSuchFieldException e) {
	    return null;
	} catch (IllegalAccessException e) {
	    return null;
	}
    }

    /**
     * Filter the mappings and only return lines matching the prefix passed in.
     * @param data the data encoding the mappings
     * @param prefix the String prefix
     * @return A list of lines that match the prefix
     */
    private static String findFirstMatchingLine(String data, String prefix) {

	if (data==null) {
	    return "";
	}
	int ix = data.indexOf(prefix);
	if (ix>=0) {
	    int iy = data.indexOf("\n", ix);
	    if (iy>0) {
		return data.substring(ix, iy);
	    }
	}
	return "";
    }

    private static String getParameterTypeName(Class<?> cls) {

	String parameterTypeNameName = cls.getName();
	int arrayNestingDepth = 0;
	int ix = parameterTypeNameName.indexOf("[");
	while (ix>-1) {
	    arrayNestingDepth++;
	    parameterTypeNameName = parameterTypeNameName.replaceFirst("(\\[\\w)|(\\[)", "");
	    ix = parameterTypeNameName.indexOf("[");
	}
	parameterTypeNameName = parameterTypeNameName.replaceFirst(";", "");
	for (int k = 0; k<arrayNestingDepth; k++) {
	    parameterTypeNameName = parameterTypeNameName+"[]";
	}
	return parameterTypeNameName;

    }
}
